<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div>主应用内容（不能被子应用样式污染）</div>
    <!-- 子应用容器 -->
    <div id="sub-container"></div>

    <script>
      console.log("主应用", window.a); // 主应用undefined
    </script>

    <script>
      function createWujieAppElement() {
        class WujieApp extends HTMLElement {
          async connectedCallback() {
            // 1. 创建沙箱
            const sandbox = createSandbox();
            // 2. 创建shadowDOM
            sandbox.shadowRoot = this.attachShadow({ mode: "open" });
            // 3. 通过请求获取到子应用内容（html、css、js）
            const { template, css, js } = await importHtml("./sub.html");
            // 4. 将html、css放入shadowDOM中
            const templateAndCSS = mergeTemplateAndCSS(template, css);
            injectTemplate(sandbox, templateAndCSS);
            // 5. 将js放入沙箱中执行
            runScriptInSandbox(sandbox, js);
          }
        }

        window.customElements.define("wujie-app", WujieApp);
        const subContainer = document.getElementById("sub-container");
        subContainer.appendChild(document.createElement("wujie-app"));
      }

      createWujieAppElement();

      // 创建沙箱
      function createSandbox() {
        const sandbox = {
          iframe: createIframe(),
          shadowRoot: null,
        };
        return sandbox;
      }

      // 创建iframe
      function createIframe() {
        const iframe = document.createElement("iframe");
        iframe.src = "about:blank"; // 默认空连接
        iframe.style.display = "none";
        document.body.appendChild(iframe);
        return iframe;
      }

      // 通过请求解析html、css、js
      async function importHtml(url) {
        const html = await Promise.resolve(fetch(url).then((res) => res.text()));
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, "text/html");

        // 提取 HTML（去除 script 和 style 标签）
        const template = extractHTML(doc);

        // 提取 CSS 内容
        const css = extractCSS(doc);

        // 提取 JS 内容
        const js = extractJS(doc);

        return {
          template,
          css,
          js,
        };
      }

      // 提取 HTML 内容（去除 script 和 style 标签）
      function extractHTML(doc) {
        const cloneDoc = doc.documentElement.cloneNode(true);
        const scripts = cloneDoc.querySelectorAll("script");
        const styles = cloneDoc.querySelectorAll("style");

        scripts.forEach((script) => script.remove());
        styles.forEach((style) => style.remove());

        return new XMLSerializer().serializeToString(cloneDoc);
      }

      // 提取 CSS 内容
      function extractCSS(doc) {
        const styleElements = doc.querySelectorAll("style");
        let cssText = "";
        styleElements.forEach((style) => {
          cssText += style.innerHTML;
        });
        return cssText;
      }

      // 提取 JS 内容
      function extractJS(doc) {
        const scriptElements = doc.querySelectorAll("script");
        let jsText = "";
        scriptElements.forEach((script) => {
          if (script.src) {
            // 如果是外链脚本，可能需要异步加载
            console.warn("外链脚本暂不支持直接提取", script.src);
          } else {
            jsText += script.innerHTML + "\n";
          }
        });
        return jsText;
      }

      // 合并模板和样式，将 CSS 插入到 HTML 的 <head> 中
      function mergeTemplateAndCSS(template, css) {
        const parser = new DOMParser();
        const doc = parser.parseFromString(template, "text/html");

        // 创建 style 元素
        const styleElement = doc.createElement("style");
        styleElement.textContent = css;

        // 将 style 插入到 head 中
        const head = doc.querySelector("head");
        if (head) {
          head.appendChild(styleElement);
        } else {
          // 如果没有 head，则创建一个并添加到 html 元素中
          const htmlElement = doc.querySelector("html");
          const headElement = doc.createElement("head");
          headElement.appendChild(styleElement);
          htmlElement.insertBefore(headElement, htmlElement.firstChild);
        }

        return new XMLSerializer().serializeToString(doc);
      }

      // 将 template 插入到 shadowRoot 中
      function injectTemplate(sandbox, template) {
        const warpper = document.createElement("div");
        warpper.innerHTML = template;
        sandbox.shadowRoot.appendChild(warpper);
      }

      // 运行js（将js放入iframe的head标签中就行）
      function runScriptInSandbox(sandbox, script) {
        const iframeWindow = sandbox.iframe.contentWindow;
        const scriptElement = iframeWindow.document.createElement("script");
        // 获取head将script插入
        const headElement = iframeWindow.document.querySelector("head");

        /**
         * 子应用中，如果打印window，获取到的是父应用的，
         * 我们希望在脚本执行之前，有些方法是父应用的，有些方法是子应用的
         * 例如1：document.querySelector('#sub-text')，就不是iframe的，而是获取shadowRoot的
         * 例如2：添加弹窗，document.createElement('div').appendChild(div)，不能在iframe中，需要代理到主应用
         * 例如3：路由也需要同步到主应用
         */
        Object.defineProperty(iframeWindow.Document.prototype, "querySelector", {
          get() {
            // 如果加载的脚本内部调用了 querySelector，则代理到 shadowRoot 中
            /**
             * 问：为什么sandbox.shadowRoot这个影子dom有querySelector属性？
             * ✅ shadowRoot 是一个 ShadowRoot 对象，它继承自 DocumentFragment 接口，
             * 在 DOM 标准中，ShadowRoot 同样拥有完整的 DOM 操作能力，包括：
                querySelector
                querySelectorAll
                getElementById
                appendChild
            */
            // 当子应用iframe调用document.querySelector，相当于调用 sandbox.shadowRoot['querySelector']
            return new Proxy(sandbox.shadowRoot["querySelector"], {
              apply(target, thisArg, args) {
                /**
                 * target：原始方法
                 * thisArg：this指向（document）
                 * args：参数
                 */
                // thisArg 是 iframe 的 document，去指向 shadowRoot
                return thisArg.querySelector.apply(sandbox.shadowRoot, args);
              },
            });
          },
        });
        scriptElement.textContent = script;
        headElement.appendChild(scriptElement);
      }
    </script>
  </body>
</html>
